spb/autoscholar
Public
1import { AppError, getInstructionVersion, uploadInstruction } from "@autoscholar/core";2import { INSTRUCTION_MAX_BYTES, UploadInstructionRequest } from "@autoscholar/schemas";3import { handler, json, parseBody } from "@/lib/server/api";4import { db } from "@/lib/server/db";5import { requireAdmin, uploadLimiter } from "@/lib/server/session";67export const runtime = "nodejs";89/** Upload (multipart `file`) or paste (JSON {content}) a research CLAUDE.md. Stores exact bytes + SHA-256 as a new immutable version. */10export const POST = handler(async (req, { params }) => {11 const sess = await requireAdmin(req, { mutation: true });12 if (!uploadLimiter.check(sess.userId).allowed) throw new AppError("rate_limited", "too many uploads", 429);13 const { id } = (await params) as { id: string };14 const ct = req.headers.get("content-type") ?? "";15 let bytes: Buffer;16 let filename = "CLAUDE.md";17 let note: string | undefined;18 if (ct.startsWith("multipart/form-data")) {19 const len = Number(req.headers.get("content-length") ?? 0);20 if (len > INSTRUCTION_MAX_BYTES * 2) throw new AppError("too_large", "upload too large", 413);21 const form = await req.formData();22 const file = form.get("file");23 if (!(file instanceof File)) throw new AppError("invalid", "multipart field `file` required", 400);24 if (file.size > INSTRUCTION_MAX_BYTES)25 throw new AppError("too_large", `file exceeds ${INSTRUCTION_MAX_BYTES} bytes`, 413);26 const allowed = ["text/markdown", "text/plain", "text/x-markdown", "application/octet-stream", ""];27 if (!allowed.includes(file.type))28 throw new AppError("invalid", `unsupported content type ${file.type}`, 400);29 bytes = Buffer.from(await file.arrayBuffer());30 filename = file.name || "CLAUDE.md";31 const n = form.get("note");32 if (typeof n === "string" && n) note = n.slice(0, 500);33 } else {34 const body = await parseBody(req, UploadInstructionRequest, INSTRUCTION_MAX_BYTES * 2);35 bytes = Buffer.from(body.content, "utf8");36 filename = body.filename;37 note = body.note;38 }39 const result = await uploadInstruction(db(), id, bytes, filename, note, { id: sess.userId, ip: sess.ip });40 const { text: _t, ...validation } = result.validation;41 return json(42 { id: result.id, version: result.version, deduplicated: result.deduplicated, validation },43 { status: result.deduplicated ? 200 : 201 },44 );45});4647export const GET = handler(async (req) => {48 await requireAdmin(req);49 const url = new URL(req.url);50 const versionId = url.searchParams.get("versionId");51 if (!versionId) throw new AppError("invalid", "versionId required", 400);52 const v = await getInstructionVersion(db(), versionId);53 return json({54 id: v.id,55 version: v.version,56 filename: v.filename,57 sha256: v.sha256,58 byteSize: v.byteSize,59 content: v.contentText,60 frontmatter: v.frontmatter,61 validation: v.validation,62 createdAt: v.createdAt,63 });64});65